﻿/** 
 * Copyright / Licence
 * <p>
 * You are free:
 * <li>to Share — to copy, distribute and transmit the work
 * <li>to Remix — to adapt the work
 * </p>
 * <p>
 * Under the following conditions:
 * <li>Attribution — You must attribute the work in the manner specified by the author or licensor (but not in any way that suggests that they endorse you or your use of the work).
 * <li>Noncommercial — You may not use this work for commercial purposes.
 * <li>Share Alike — If you alter, transform, or build upon this work, you may distribute the resulting work only under the same or similar license to this one.
 * </p>
 * <p>
 * More information can be found here:
 * <br/>
 * http://creativecommons.org/licenses/by-nc-sa/3.0/
 * </p>
 * <p>
 * Please contact Manfred Weber [manfred.weber@gmail.com] if you require a commercial licence
 * </p>
 */
package org.dschini.ui.maps {

	import caurina.transitions.*;
	
	import flash.display.*;
	import flash.events.*;
	import flash.geom.*;
	import flash.utils.*;
	
	import org.dschini.data.ui.maps.*;
	
	/**
	 * The MercatorProjection holds several functions to implement a Mercator Viewer and is of type Sprite.
	 * If you want to use the MercatorProjection in Flex you need to wrap it into a UIComponent
	 * 
	 * <p>
	 * <code>
	 * var mercatorProjection:MercatorProjection = new MercatorProjection();
	 * <br/>
	 * addChild( mercatorProjection );
	 * </code>
	 * </p>
	 */
	public class MercatorProjection extends Sprite
	{
		/**
		 * Array holding all the existing markers
		 */
		public var markers:Array;
		
		/**
		 * Array holding all the existing connections
		 */
		public var connections:Array;
		
		/**
		 * Array holding all the existing circles
		 */
		public var circles:Array;
		
		/**
		 * @private
		 * 
		 * Do not use the viewport directly.
		 * It is the  mask for the map
		 */
		public var viewport:Sprite;
		
		/**
		 * @private
		 * 
		 * Do not use the itemsMask directly.
		 * It is the  mask for items like markers, connections, circle, ...
		 */
		public var itemsMask:Sprite;
		
		/**
		 * @private
		 * 
		 * The itemsContainer contains items like markers, connections, circle, ...
		 */
		public var itemsContainer:Sprite;
		
		/**
		 * The world Map
		 */
		public var map:WorldMap;
		
		/**
		 * @private
		 * 
		 * The Background 
		 */
		public var background:Sprite;
		
		/**
		 * Dragging status
		 */
		public var isDragging:Boolean;
		
		/**
		 * The radius of the world in pixels
		 */
		public var offset:Number;
		
		/**
		 * @private
		 * 
		 * The Radians of offset
		 */
		public var radius:Number;
		
		/**
		 * The radius of the world in km
		 */
		public var R:Number;
		
		/**
		 * @private
		 */
		private var _zoom:Number;

		/**
		 * Creates a new MercatorProjection
		 */
		public function MercatorProjection():void
		{
			mouseEnabled = true;
			doubleClickEnabled = true;
			offset = 268435456;
			radius = offset/Math.PI;
			R = 6371.0;
			_zoom = 1;
			markers = new Array();
			connections = new Array();
			circles = new Array();
			viewport.width = viewport.height = itemsContainer.width = itemsContainer.height = background.width = background.height = 400;
			map.mask = viewport;
			itemsContainer.mask = itemsMask;
			
			addEventListener( MouseEvent.CLICK, mouseClickHandler );
			addEventListener( MouseEvent.DOUBLE_CLICK, mouseDoubleClickHandler );
			addEventListener( MouseEvent.MOUSE_DOWN, mouseDownHandler );
			addEventListener( MouseEvent.MOUSE_MOVE, mouseMoveHandler );
			addEventListener( MouseEvent.MOUSE_OUT, mouseOutHandler );
			addEventListener( MouseEvent.MOUSE_OVER, mouseOverHandler );
			addEventListener( MouseEvent.MOUSE_UP, mouseUpHandler );
			addEventListener( MouseEvent.MOUSE_WHEEL, mouseWheelHandler );
		}
		
		/**
		 * Enabled / Disabled Double Click
		 */
		override public function set doubleClickEnabled( doubleClickEnabled:Boolean ):void
		{
			super.doubleClickEnabled = doubleClickEnabled;
			background.doubleClickEnabled = doubleClickEnabled;
			map.doubleClickEnabled = doubleClickEnabled;
		}
		
		/**
		 * Enabled / Disabled Mouse
		 */
		override public function set mouseEnabled( mouseEnabled:Boolean ):void
		{
			super.mouseEnabled = mouseEnabled;
			viewport.mouseEnabled = mouseEnabled;
			background.mouseEnabled = mouseEnabled;
			map.mouseEnabled = mouseEnabled;
		}
		
		/**
		 * Start Drag
		 */
		override public function startDrag(lockCenter:Boolean = false, bounds:Rectangle = null):void
		{
			isDragging = true;
			map.startDrag( lockCenter, bounds );
			addEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler);
		}
		
		/**
		 * Stop Drag
		 */
		override public function stopDrag():void
		{
			isDragging = false;
			map.stopDrag();
			removeEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler);
		}
		
		/**
		 * @private
		 * 
		 * Internal Mouse Click Handler
		 */
		private function getMapDataPointFromMouse( event:MouseEvent ):MapPointData
		{
			var mapPointData:MapPointData = new MapPointData();
			mapPointData.lon = getLonFromX(map.mouseX);
			mapPointData.lat = getLatFromY(map.mouseY);
			mapPointData.x = map.mouseX;
			mapPointData.y = map.mouseY;
			mapPointData.target = event.target.parent ? event.target.parent : null;
			return mapPointData;
		}
		
		/**
		 * @private
		 * 
		 * Internal Mouse Click Handler
		 */
		private function mouseClickHandler( event:MouseEvent ):void
		{ 
			dispatchEvent( new MapEvent( event, getMapDataPointFromMouse( event ) , MapEvent.CLICK ) ) 
		};
		
		/**
		 * @private
		 * 
		 * Internal Mouse Double Click Handler
		 */
		private function mouseDoubleClickHandler( event:MouseEvent ):void
		{ 
			dispatchEvent( new MapEvent( event, getMapDataPointFromMouse( event ) , MapEvent.DOUBLE_CLICK ) ) 
		};
		
		/**
		 * @private
		 * 
		 * Internal Mouse Down Handler
		 */
		private function mouseDownHandler( event:MouseEvent ):void
		{ 
			dispatchEvent( new MapEvent( event, getMapDataPointFromMouse( event ), MapEvent.MOUSE_DOWN ) ) 
		};
		
		/**
		 * @private
		 * 
		 * Internal Mouse Up Handler
		 */
		private function mouseUpHandler( event:MouseEvent ):void
		{ 
			dispatchEvent( new MapEvent( event, getMapDataPointFromMouse( event ), MapEvent.MOUSE_UP ) ) 
		};
		
		/**
		 * @private
		 * 
		 * Internal Mouse Over Handler
		 */
		private function mouseOverHandler( event:MouseEvent ):void
		{ 
			dispatchEvent( new MapEvent( event, getMapDataPointFromMouse( event ), MapEvent.MOUSE_OVER ) ) 
		};
		
		/**
		 * @private
		 * 
		 * Internal Mouse Out Handler
		 */
		private function mouseOutHandler( event:MouseEvent ):void
		{ 
			dispatchEvent( new MapEvent( event, getMapDataPointFromMouse( event ), MapEvent.MOUSE_OUT ) ) 
		};
		
		/**
		 * @private
		 * 
		 * Internal Mouse Move Handler
		 */
		private function mouseMoveHandler( event:MouseEvent ):void
		{
			if( isDragging ){
				itemsContainer.x = map.x
				itemsContainer.y = map.y
				event.updateAfterEvent();
			}
			dispatchEvent( new MapEvent( event, getMapDataPointFromMouse( event ), MapEvent.MOUSE_MOVE ) ) 
		};
		
		/**
		 * @private
		 * 
		 * Internal Mouse Wheel Handler
		 */
		private function mouseWheelHandler( event:MouseEvent ):void
		{
			dispatchEvent( new MapEvent( event, getMapDataPointFromMouse( event ), MapEvent.MOUSE_WHEEL ) ) 
		};
		
		/**
		 * Getter/Setter zoomin the map from the center point.
		 * It is always better to use the setCenter() function instead
		 */
		public function set zoom( zoom:Number ):void {
			_zoom = itemsContainer.scaleX = itemsContainer.scaleY = map.scaleX = map.scaleY = zoom;
		}
		
		/**
		 * Getter/Setter zoomin the map from the center point.
		 * It is always better to use the setCenter() function instead
		 */
		public function get zoom():Number { return _zoom; }
		
		/**
		 * @private
		 * 
		 * Use scale instead
		 */
		override public function set scaleX( scaleX:Number ):void { trace("Use zoom"); }
		
		/**
		 * @private
		 * 
		 * Use scale instead
		 */
		override public function get scaleX():Number { return _zoom; }
		
		/**
		 * @private
		 * 
		 * Use scale instead
		 */
		override public function set scaleY( scaleY:Number ):void { trace("Use zoom");  }
		
		/**
		 * @private
		 * 
		 * Use scale instead
		 */
		override public function get scaleY():Number { return _zoom; }
		
		/**
		 * Getter/Setter the width of the map.
		 */
		override public function set width( newWidth:Number ):void { 
			viewport.width = newWidth;
			itemsMask.width = newWidth;
			itemsContainer.width  = newWidth;
			background.width = newWidth;
			zoom = zoom;
		}
		
		/**
		 * Getter/Setter the width of the map.
		 */
		override public function get width():Number { 
			return viewport.width;
		}
		
		/**
		 * Getter/Sette the height of the map.
		 */
		override public function set height( newHeight:Number ):void {
			viewport.height = newHeight;
			itemsMask.height = newHeight;
			itemsContainer.height  = newHeight;
			background.height = newHeight;
			zoom = zoom;
		}
		
		/**
		 * Getter/Sette the height of the map.
		 */
		override public function get height():Number {
			return viewport.height;
		}
		
		/**
		 * @private
		 * 
		 * Convert Longitude to X
		 * Use getLongitudeFromX instead
		 */
		private function LToX(x:Number):Number { return Math.round(offset+radius*x*Math.PI/180); }
		
		/**
		 * @private
		 * 
		 * Convert Latitude to Y
		 * Use getLatitudeFromY instead
		 */
		private function LToY(y:Number):Number { return Math.round(offset-radius*Math.log((1+Math.sin(y*Math.PI/180))/(1-Math.sin(y*Math.PI/180)))/2); }
		
		/**
		 * @private
		 * 
		 * Convert X to Longitude
		 * Use getLongitudeFromX instead
		 */
		private function XToL(x:Number):Number { return ((Math.round(x)-offset)/radius)*180/Math.PI; }
		
		/**
		 * @private
		 * 
		 * Convert Y to Latitude
		 * Use getLatitudeFromY instead
		 */
		private function YToL(y:Number):Number { return (Math.PI/2-2*Math.atan(Math.exp((Math.round(y)-offset)/radius)))*180/Math.PI; }
		
		/**
		 * @private
		 */
		 private function DecimalDegreesToRadians(degrees:Number):Number  { return degrees * Math.PI / 180.0; }
		
		/**
		 * @private
		 */
		private function RadiansToDecimalDegrees(radians:Number):Number  { return radians * 180.0 / Math.PI; }

		/**
		* Calculates the x Number from longitude where map offset included
		*/
		public function getXFromLon( lon:Number ):Number { return 400/offset*(LToX( lon )- offset) }
		
		/**
		* Calculates the y Number from latitude where map offset included
		*/
		public function getYFromLat( lat:Number ):Number { return 400/offset*(LToY( lat )- offset) }
		
		/**
		* Calculates the longitude from x where map offset included
		*/
		public function getLonFromX( x:Number ):Number { return XToL( (x*(offset/400)) + offset ) }
		
		/**
		* Calculates the latitude from y where map offset included
		*/
		public function getLatFromY( y:Number ):Number { return YToL( (y*(offset/400)) + offset ) }
		
		/**
		* Calculate the distance in kilometers between two MapPointData objects based on R
		*/
		public function getDistanceKM( mapPointData1:MapPointData, mapPointData2:MapPointData ):Number
		{
			var d1:Number = getDistance( mapPointData1, mapPointData2 );
			return d1*R;
		}
		
		/**
		* Calculate the distance in pixels between two MapPointData objects based on offset
		*/
		public function getDistance( mapPointData1:MapPointData, mapPointData2:MapPointData ):Number
		{
			var lat1:Number = DecimalDegreesToRadians(mapPointData1.lat);
			var lon1:Number = DecimalDegreesToRadians(mapPointData1.lon);
			var lat2:Number = DecimalDegreesToRadians(mapPointData2.lat);
			var lon2:Number = DecimalDegreesToRadians(mapPointData2.lon);
			var d1:Number = Math.acos(Math.sin(lat1)*Math.sin(lat2)+Math.cos(lat1)*Math.cos(lat2)*Math.cos(lon1-lon2));
			return d1;
		}

		/**
		* Calculates the best zoom factor of a map
		*/
		public function getZoom( targetMap:Map ):Number
		{
			var _boundsMap:Rectangle = targetMap.getBounds( map );
			return width/_boundsMap.width <= height/_boundsMap.height
				? width / _boundsMap.width
				: height / _boundsMap.height
				;
		}

		/**
		* Calculates the point of center of a map
		*/
		public function getCenter( targetMap:Map ):Point
		{
			var coordinates:Rectangle = targetMap.getBounds( map );
			return new Point(
				coordinates.x + coordinates.width/2,
				coordinates.y + coordinates.height/2
			);
		}

		/**
		* Same as <code>scaleMarker</code> this functions scales all markers together
		*
		* @param scale the new scale of marker
		* @param delay the delay of the tween
		* @param time the time of the tween
		* @param transition the transition of the tween
		*/
		public function scaleAllMarkers( scale:Number=1, delay:Number=0, time:Number=1, transition:String="linear" ):void
		{
			for( var i:int=0; i<markers.length; i++){
				scaleMarker( markers[i].marker, scale, delay, time, transition );
			}
		}
		
		/**
		* Scaling a map does not automatically scale markers. You have to explicitely scale them.
		* Calling this functions with no parameters divides the markers scale with 1
		*
		* @param marker the marker Sprite
		* @param scale the new scale of marker
		* @param delay the delay of the tween
		* @param time the time of the tween
		* @param transition the transition of the tween
		*/
		public function scaleMarker( marker:Sprite, scale:Number=1, delay:Number=0, time:Number=1, transition:String="linear" ):void
		{
			if( time > 0 ){
				Tweener.removeTweens( marker );
				Tweener.addTween(marker, {scaleX:1/zoom*scale, time:time, delay:delay, transition:transition});
				Tweener.addTween(marker, {scaleY:1/zoom*scale, time:time, delay:delay, transition:transition});
			} else {
				marker.scaleX = marker.scaleY =1/zoom*scale;
			}
		}
		
		/**
		* Set the center of the map.
		* 
		* Setting the time to 0 will center immediately
		*
		* @param point the new center of map
		* @param newZoom the new zoom of map
		* @param delay the delay of the tween
		* @param time the time of the tween
		* @param transition the transition of the tween
		*/
		public function setCenter( point:Point, newZoom:Number, delay:Number=0, time:Number=1, transition:String="linear" ):void
		{
			Tweener.removeTweens( map );
			Tweener.removeTweens( itemsContainer );
			if( time > 0 ){
				Tweener.addTween(map, {x:-point.x*newZoom, time:time, delay:delay, transition:transition});
				Tweener.addTween(map, {y:-point.y*newZoom, time:time, delay:delay, transition:transition});
				Tweener.addTween(map, {scaleX:newZoom, time:time, delay:delay, transition:transition});
				Tweener.addTween(map, {scaleY:newZoom, time:time, delay:delay, transition:transition});
				Tweener.addTween(itemsContainer, {x:-point.x*newZoom, time:time, delay:delay, transition:transition});
				Tweener.addTween(itemsContainer, {y:-point.y*newZoom, time:time, delay:delay, transition:transition});
				Tweener.addTween(itemsContainer, {scaleX:newZoom, time:time, delay:delay, transition:transition});
				Tweener.addTween(itemsContainer, {scaleY:newZoom, time:time, delay:delay, transition:transition});
				this._zoom = newZoom;
			} else {
				map.x = -point.x*newZoom;
				map.y = -point.y*newZoom;
				itemsContainer.x = -point.x*newZoom;
				itemsContainer.y = -point.y*newZoom;
				this.zoom = newZoom;
			}
		}

		/**
		* Add a marker
		* The marker will be added to the markers Array in the following format
	 	* 
		* <code>
		* {data:mapPointData1, marker:marker1 }
		* {data:mapPointData2, marker:marker2 }
		* ...
		* </code>   
		*
		* @param MapPointData
		* @param marker a Sprite
		*/
		public function addMarker( mapPointData:MapPointData, marker:Sprite=null ):void
		{
			var targetX:Number = getXFromLon( mapPointData.lon);
			var targetY:Number = getYFromLat( mapPointData.lat);
			if( marker==null ){
				var marker:Sprite = new Sprite();
				marker.graphics.beginFill( 0xcc0000 );
				marker.graphics.drawCircle(0, 0, 1);
				marker.graphics.endFill();
			}
			marker.x = targetX;
			marker.y = targetY
			itemsContainer.addChild( marker );
	
			markers.push( { data:mapPointData, marker:marker } );
		}

		/**
		* Add a connection between two MapPointData objects
		* The connection will be added to the connections Array in the following format
	 	* 
		* <code>
		* {data1:mapPointData1, data2:mapPointData2, connection:connection1}
		* {data1:mapPointData3, data2:mapPointData4, connection:connection2}
		* ...
		* </code>   
		*
		* @param MapPointData Source
		* @param MapPointData Target
		* @param color the color of the connection
		*/
		public function addConnection( mapPointData1:MapPointData, mapPointData2:MapPointData, color:uint=0xff0000 ):void
		{
			//var distance:Number = getDistance( mapMarkerData1, mapMarkerData2 );
			
			var sourceX:Number = getXFromLon( mapPointData1.lon);
			var sourceY:Number = getYFromLat( mapPointData1.lat);
			var targetX:Number = getXFromLon( mapPointData2.lon);
			var targetY:Number = getYFromLat( mapPointData2.lat);
			var connection:Sprite = new Sprite();
			connection.graphics.lineStyle( 0.5, color );
			connection.graphics.moveTo( sourceX, sourceY );
			connection.graphics.lineTo( targetX, targetY );
			itemsContainer.addChild( connection );

			connections.push( {data1:mapPointData1, data2:mapPointData2, connection:connection} );
		}

		/**
		* Add a circle between two MapPointData objects
		* The circle will be added to the circles Array in the following format
	 	* 
		* <code>
		* {data1:mapPointData1, data2:mapPointData2, circle:circle}
		* {data1:mapPointData3, data2:mapPointData4, circle:circle}
		* ...
		* </code>   
		*
		* @param MapPointData Source
		* @param MapPointData Target
		* @param color the color of the circle
		*/
		public function addCircle( data1:MapPointData, data2:MapPointData, color:uint=0xff0000 ):void
		{
			//var distance:Number = getDistance( mapMarkerData1, mapMarkerData2 );
			var sourceX:Number = getXFromLon( data1.lon);
			var sourceY:Number = getYFromLat( data1.lat);
			var targetX:Number = getXFromLon( data2.lon);
			var targetY:Number = getYFromLat( data2.lat);

			var radius:Number;
			var dx:Number;
			var dy:Number;
			dx = sourceX-targetX;
			dy = sourceY-targetY;
			radius = Math.sqrt(dx*dx + dy*dy);

			var circle:Sprite = new Sprite();
			circle.graphics.lineStyle( 0.5, color );
			circle.graphics.drawCircle( sourceX, sourceY, radius );
			itemsContainer.addChild( circle );
			
			circles.push( {data1:data1, data2:data2, circle:circle} );
		}
		
		/**
		* Removes all markers from display list, clear markers Array, set markers to null
		*/
		public function removeAllMarkers():void
		{
			for( var i:int=0; i<markers.length; i++){
				itemsContainer.removeChild(markers[i].marker);
				markers[i].marker = null;
				markers[i] = null;
			}
			markers = new Array();
		}
		
		/**
		* Removes all connections from display list, clear connections Array, set connections to null
		*/
		public function removeAllConnections():void
		{
			for( var i:int=0; i<connections.length; i++){
				itemsContainer.removeChild(connections[i].connection);
				connections[i].connection = null;
				connections[i] = null;
			}
			connections = new Array();
		}
		
		/**
		* Removes all circles from display list, clear circles Array, set circles to null
		*/
		public function removeAllCircles():void
		{
			for( var i:int=0; i<circles.length; i++){
				itemsContainer.removeChild(circles[i].circle);
				circles[i].circle = null;
				circles[i] = null;
			}
			circles = new Array();
		}
		
		/**
		* Render the projection. Use this function whenever you changed the data of a map!
		* This will refresh all the maps and submaps
		*/
		public function render():void {
			if( map.data ){
				if( map.data.backgroundColorTransform ){
					background.transform.colorTransform = map.data.backgroundColorTransform;
				}
				background.visible = map.data.backgroundVisible;
				map.render();
			}
		}
	
		/**
		* Clean the map
		*/
		public function dispose():void
		{
			removeEventListener( MouseEvent.CLICK, mouseClickHandler );
			removeEventListener( MouseEvent.DOUBLE_CLICK, mouseDoubleClickHandler );
			removeEventListener( MouseEvent.MOUSE_DOWN, mouseDownHandler );
			removeEventListener( MouseEvent.MOUSE_MOVE, mouseMoveHandler );
			removeEventListener( MouseEvent.MOUSE_OUT, mouseOutHandler );
			removeEventListener( MouseEvent.MOUSE_OVER, mouseOverHandler );
			removeEventListener( MouseEvent.MOUSE_UP, mouseUpHandler );
			removeEventListener( MouseEvent.MOUSE_WHEEL, mouseWheelHandler );
		}
	}	
}
